本系列文章已出版實體書籍:
「你的地圖會說話?WebGIS 與 JavaScript 的情感交織」(博碩文化)
WebGIS啟蒙首選✖五家地圖API✖近百個程式範例✖實用簡易口訣✖學習難度分級✖補充ES6小知識
本篇文章請搭配
[3-1] 打造你的美食地圖!用Here Maps API 秀出Google API餐廳資訊
講到Scope Chain(範圍鏈),就要先來講講Scope。有別於其他程式語言,JS是以function為主體的一種程式語言。
也就是說,在一個function的開始與結束,會形成一個作用域(Scope),而在function外的就是它的外部環境(Outer Environment)。
讓我們來舉個例子。
var param = 'Global';
function Outer() {
var param = 'Outer';
console.log(`Scope: ${param}`);
function Middle() {
var param = 'Middle';
console.log(`Scope: ${param}`);
function Inner() {
var param = 'Inner';
console.log(`Scope: ${param}`);
}
Inner(); // 呼叫Middle時,呼叫Inner
}
Middle(); // 呼叫Outer時,呼叫Middle
}
↑ 建立多個function包function,在function外部的稱為全域環境(Global),第一層為Outer,第二層為Middle,最裡層為Inner。而每層都有相同的變數param。
console.log(`Scope: ${param}`); // Global
Outer();
↑ 呼叫Outer。可以預期最外層的param等於Global,因為它在全域環境。
↑ 結果如預料之中,最外層為Global,Outer層的Scope為Outer,Middle層的Scope為Middle,Inner層的Scope為Inner。現在我們把Inner中的param拿掉。
function Outer() {
var param = 'Outer';
console.log(`Scope: ${param}`);
function Middle() {
var param = 'Middle';
console.log(`Scope: ${param}`);
function Inner() {
//var param = 'Inner';
// Inner Scope沒有param,往外層找到Middle
console.log(`Scope: ${param}`);
}
Inner(); // 呼叫Middle時,呼叫Inner
}
Middle(); // 呼叫Outer時,呼叫Middle
}
↑ 當Inner層在Inner Scope找不到param的時候,就會往外找它的外部環境,於是找到了Middle層。接者我們把Middle中的param也拿掉。
function Outer() {
var param = 'Outer';
console.log(`Scope: ${param}`);
function Middle() {
//var param = 'Middle';
// Middle Scope沒有param,往外層找到Outer
console.log(`Scope: ${param}`);
function Inner() {
//var param = 'Inner';
// Inner Scope沒有param,往外層找到Middle,Middle Scope也沒有param,於是再往外層找到Outer Scope層
console.log(`Scope: ${param}`);
}
Inner(); // 呼叫Middle時,呼叫Inner
}
Middle(); // 呼叫Outer時,呼叫Middle
}
↑ 當Inner Scope沒有param時,往外層找到Middle Scope也沒有param,於是再往外層找到Outer Scope。由此可知,作用域(Scope)會由內而外,一層一層向外尋找,直到找到為止,這由內而外尋找的過程就稱為Scope Chain(範圍鏈)。如果直到全域環境依舊沒有找到,就會回傳undefined。
var param = 'Global';
function Outer() {
//var param = 'Outer';
console.log(`Scope: ${param}`);
function Middle() {
//var param = 'Middle';
console.log(`Scope: ${param}`);
function Inner() {
//var param = 'Inner';
console.log(`Scope: ${param}`);
}
Inner();
}
Middle();
}
console.log(`Scope: ${param}`);
Outer();
↑ 把Outer也註解。
↑ 於是全部找到Global。
還記得昨天最後面以Here Maps API寫的展點嗎?讓我們來回顧一下function。
var ShowMultiPoint = (dataList = [], map) => {
if (dataList.length > 0) {
dataList.forEach(item => {
let marker = new H.map.Marker({ lat: item.y, lng: item.x });
map.addObject(marker);
marker.setData(`<div class="infoWindow">
<h2>${item.name}</h2>
<p>經度: ${item.x}</p>
<p>緯度: ${item.y}</p>
<p>地址: ${item.address}</p>
</div>
`);
marker.addEventListener('tap', (evt) => {
let bubble = new H.ui.InfoBubble(evt.target.getGeometry(), {
content: evt.target.getData()
});
ui.addBubble(bubble);
});
});
}
}
↑ 這是我們昨天寫的函式,傳入點資料陣列以及地圖,跑一個迴圈把每個點資料建立成marker,並且給每個marker監聽事件,當它們點擊的時候,會秀出他們的資訊視窗。這個function是用ES6的寫法,我們先把它們轉成ES5,比較好說明。
function ShowMultiPoint(dataList, map) {
dataList = dataList || [];
if (dataList.length > 0) {
for (var i = 0; i < dataList.length; i++) {
var marker = new H.map.Marker({ lat: dataList[i].y, lng: dataList[i].x });
var content = `<div class="infoWindow">
<h2>${dataList[i].name}</h2>
<p>經度: ${dataList[i].x}</p>
<p>緯度: ${dataList[i].y}</p>
<p>地址: ${dataList[i].address}</p>
</div>
`; // ES6樣板語法太好用了,繼續寫
map.addObject(marker);
marker.addEventListener('tap', function (evt) {
var bubble = new H.ui.InfoBubble(evt.target.getGeometry(), {
content: content
});
ui.addBubble(bubble);
});
}
}
}
讓我們~繼續~看~下~去~
↑ 結果全部的資訊視窗都變成貳樓餐廳了,怎麼有這種事? 該不會被貳樓駭客入侵
用chrome F12開發者工具下中斷點偵錯就會發現,for迴圈會依照資料的筆數,假如有20筆就會跑20次,並解建立20個點以及20個監聽事件。可是監聽事件只是被建立,而不是被觸發。當監聽事件被觸發時i已經變成最大值,因此怎麼抓都只抓到最後一筆的資訊。
↑ i已經變成最大值19,永遠都抓到最後一筆貳樓餐廳。
那究竟該怎麼辦呢?別擔心,這裡提供大家四種解法!
為什麼要用forEach呢?因為Array.prototype.forEach()方法,裡面接的參數為callback函式,讓我們看看mdn的定義。
↑ 可以看到,forEach的內容其實是提供陣列每次迭帶的回呼函式,而每個回呼函式都是一個function,無意間形成了一個作用域(scope),讓每一個item值都被保存在這個scope中。
function ShowMultiPoint(dataList, map) {
dataList = dataList || [];
if (dataList.length > 0) {
dataList.forEach(function (item) { // Scope起始,item在Scope內被保存
var marker = new H.map.Marker({ lat: item.y, lng: item.x });
var content = `<div class="infoWindow">
<h2>${item.name}</h2>
<p>經度: ${item.x}</p>
<p>緯度: ${item.y}</p>
<p>地址: ${item.address}</p>
</div>
`;
map.addObject(marker);
marker.addEventListener('tap', function (evt) {
let bubble = new H.ui.InfoBubble(evt.target.getGeometry(), {
content: content
});
ui.addBubble(bubble);
});
}); // Scope結束
}
}
↑ forEach後面的回調函式,形成一個scope,並且為外層監聽事件提供Outer Environment,每個item將會被每一個迭帶的scope分別保存。Javascript有回收機制,當偵測到function內容沒有重複再使用時,會因記憶體的考量而進行回收。如果該function會再繼續使用,並且它的外部環境依舊存在變數參考時,外部環境的作用域也不會回收,雖然可以保存特定資訊,但會對記憶體有負擔。
↑ 成功保存每個資訊視窗的內容
function ShowMultiPoint(dataList, map) {
dataList = dataList || [];
console.log(content)
if (dataList.length > 0) {
for (var i = 0; i < dataList.length; i++) {
var marker = new H.map.Marker({ lat: dataList[i].y, lng: dataList[i].x });
var content = `<div class="infoWindow">
<h2>${dataList[i].name}</h2>
<p>經度: ${dataList[i].x}</p>
<p>緯度: ${dataList[i].y}</p>
<p>地址: ${dataList[i].address}</p>
</div>
`;
map.addObject(marker);
function AddListener(content) { // Scope起始,content在Scope內被保存
marker.addEventListener('tap', function (evt) {
var bubble = new H.ui.InfoBubble(evt.target.getGeometry(), {
content: content
});
ui.addBubble(bubble);
});
} // Scope結束
AddListener(content);
}
}
}
↑ 在function內建構一個名為AddListener的具名函式,形成function包function的現象,稱為閉包(closure)。呼叫AddListener時傳入想要保存的參數,就會在AddListener的scope內儲存。閉包運用可以讓外部環境透過function部分存取內部環境的變數,卻不能任意更動它,可以有效達成封裝的效果。
IIFE全名為Immediately invoked function expression,稱為立即函式。
(function () {
console.log("我是立即函式!");
})();
↑ 立即函式也就是只當下立刻執行,會用兩個小括弧去觸發。第一個小括弧放入要執行的函式,第二個小括弧讓它立刻執行。特別注意!立即函式中的函式只能放匿名函式,不能放具名函式。
function ShowMultiPoint(dataList, map) {
dataList = dataList || [];
if (dataList.length > 0) {
for (var i = 0; i < dataList.length; i++) {
var marker = new H.map.Marker({ lat: dataList[i].y, lng: dataList[i].x });
var content = `<div class="infoWindow">
<h2>${dataList[i].name}</h2>
<p>經度: ${dataList[i].x}</p>
<p>緯度: ${dataList[i].y}</p>
<p>地址: ${dataList[i].address}</p>
</div>
`;
map.addObject(marker);
(function (content) { // 內部匿名函式接取的parameter
marker.addEventListener('tap', function (evt) {
var bubble = new H.ui.InfoBubble(evt.target.getGeometry(), {
content: content
});
ui.addBubble(bubble);
});
})(content); // 外部傳入立即函式的參數
}
}
}
把監聽事件外層披上一層IIFE的外衣,並且把content傳入,也可以在立即函式中形成一個scope,保存content。
在ES6新增了let跟const宣告變數的方式,而不僅僅只有var,const很直覺就是常數,無法任意改變及取代。那麼let跟var有什麼區別呢?
var的作用域為function,而let的作用域卻是大括號。
function ShowMultiPoint(dataList, map) {
dataList = dataList || [];
//console.log(content); // content is not defined
if (dataList.length > 0) {
for (let i = 0; i < dataList.length; i++) { // Scope起始
let marker = new H.map.Marker({ lat: dataList[i].y, lng: dataList[i].x });
let content = `<div class="infoWindow">
<h2>${dataList[i].name}</h2>
<p>經度: ${dataList[i].x}</p>
<p>緯度: ${dataList[i].y}</p>
<p>地址: ${dataList[i].address}</p>
</div>
`;
map.addObject(marker);
marker.addEventListener('tap', function (evt) {
var bubble = new H.ui.InfoBubble(evt.target.getGeometry(), {
content: content
});
ui.addBubble(bubble);
});
} // Scope結束
}
}
↑ 把變數都改成以let宣告
因為let作用域為大括弧,因此作用域範圍就變成在for迴圈之內。
不知道大家會不會有個疑問?作用域變成大括弧,這樣不就完全顛覆了剛剛講的以function為主體的Scope Chain了嗎?倒也不然,ES6的let其實也是個語法糖,它雖然看似只會在大括弧內作用,但因為hoisting(提升)的關係,它其實在function作用域內的最上面就已經被宣告了,只是形成了暫時性死區(TDZ)。
↑ 暫時性死區(TDZ),全名為Temporal Dead Zone,是let宣告的變數從宣告位置到function作用域起始中間的區域。
今日延續昨天Here Maps API監聽事件可能會碰到的問題及解法,幫大家整理如下:
明日再戰!